1
|
|
|
import React from "react"; |
2
|
|
|
import { |
3
|
|
|
injectIntl, |
4
|
|
|
WrappedComponentProps, |
5
|
|
|
FormattedMessage, |
6
|
|
|
defineMessages, |
7
|
|
|
} from "react-intl"; |
8
|
|
|
import className from "classnames"; |
9
|
|
|
import Swal from "sweetalert2"; |
10
|
|
|
import * as routes from "../../helpers/routes"; |
11
|
|
|
import Select, { SelectOption } from "../Select"; |
12
|
|
|
import { Application } from "../../models/types"; |
13
|
|
|
import { ReviewStatusId } from "../../models/lookupConstants"; |
14
|
|
|
|
15
|
|
|
const messages = defineMessages({ |
16
|
|
|
veteranLogo: { |
17
|
|
|
id: "veteranStatus.veteranLogoAlt", |
18
|
|
|
defaultMessage: "Talent cloud veteran logo", |
19
|
|
|
description: "Alt Text for Veteran Logo Img", |
20
|
|
|
}, |
21
|
|
|
emailCandidate: { |
22
|
|
|
id: "apl.emailCandidateLinkTitle", |
23
|
|
|
defaultMessage: "Email this candidate.", |
24
|
|
|
description: "Title, hover text, for email link.", |
25
|
|
|
}, |
26
|
|
|
viewApplicationTitle: { |
27
|
|
|
id: "apl.viewApplicationLinkTitle", |
28
|
|
|
defaultMessage: "View this applicant's application.", |
29
|
|
|
description: "Title, hover text, for View Application Link", |
30
|
|
|
}, |
31
|
|
|
viewProfileTitle: { |
32
|
|
|
id: "apl.viewProfileLinkTitle", |
33
|
|
|
defaultMessage: "View this applicant's profile.", |
34
|
|
|
description: "Title, hover text, for View Profile Link", |
35
|
|
|
}, |
36
|
|
|
decision: { |
37
|
|
|
id: "apl.decision", |
38
|
|
|
defaultMessage: "Decision", |
39
|
|
|
description: "Decision dropdown label", |
40
|
|
|
}, |
41
|
|
|
notReviewed: { |
42
|
|
|
id: "reviewStatus.notReviewed", |
43
|
|
|
defaultMessage: "Not Reviewed", |
44
|
|
|
description: "Decision dropdown label", |
45
|
|
|
}, |
46
|
|
|
saving: { |
47
|
|
|
id: "button.saving", |
48
|
|
|
defaultMessage: "Saving...", |
49
|
|
|
description: "Dynamic Save button label", |
50
|
|
|
}, |
51
|
|
|
save: { |
52
|
|
|
id: "button.save", |
53
|
|
|
defaultMessage: "Save", |
54
|
|
|
description: "Dynamic Save button label", |
55
|
|
|
}, |
56
|
|
|
saved: { |
57
|
|
|
id: "button.saved", |
58
|
|
|
defaultMessage: "Saved", |
59
|
|
|
description: "Dynamic Save button label", |
60
|
|
|
}, |
61
|
|
|
addNote: { |
62
|
|
|
id: "apl.addNote", |
63
|
|
|
defaultMessage: "+ Add a Note", |
64
|
|
|
description: "Dynamic Note button label", |
65
|
|
|
}, |
66
|
|
|
editNote: { |
67
|
|
|
id: "apl.editNote", |
68
|
|
|
defaultMessage: "Edit Note", |
69
|
|
|
description: "Dynamic Note button label", |
70
|
|
|
}, |
71
|
|
|
screenedOut: { |
72
|
|
|
id: "reviewStatus.screenedOut", |
73
|
|
|
defaultMessage: "Screened Out", |
74
|
|
|
description: "Dynamic Note button label", |
75
|
|
|
}, |
76
|
|
|
cancelButton: { |
77
|
|
|
id: "button.cancel", |
78
|
|
|
defaultMessage: "Cancel", |
79
|
|
|
description: "Cancel button label", |
80
|
|
|
}, |
81
|
|
|
stillThinking: { |
82
|
|
|
id: "reviewStatus.stillThinking", |
83
|
|
|
defaultMessage: "Still Thinking", |
84
|
|
|
description: "Dynamic Note button label", |
85
|
|
|
}, |
86
|
|
|
stillIn: { |
87
|
|
|
id: "reviewStatus.stillIn", |
88
|
|
|
defaultMessage: "Still In", |
89
|
|
|
description: "Dynamic Note button label", |
90
|
|
|
}, |
91
|
|
|
confirmButton: { |
92
|
|
|
id: "button.confirm", |
93
|
|
|
defaultMessage: "Confirm", |
94
|
|
|
description: "Confirm button for modal dialogue boxes", |
95
|
|
|
}, |
96
|
|
|
screenOutConfirm: { |
97
|
|
|
id: "apl.screenOutConfirm", |
98
|
|
|
defaultMessage: "Screen out the candidate?", |
99
|
|
|
description: "Are you sure you want to screen out the candidate warning", |
100
|
|
|
}, |
101
|
|
|
screenInConfirm: { |
102
|
|
|
id: "apl.screenInConfirm", |
103
|
|
|
defaultMessage: "Screen the candidate back in?", |
104
|
|
|
description: "Are you sure you want to screen in the candidate warning", |
105
|
|
|
}, |
106
|
|
|
}); |
107
|
|
|
|
108
|
|
|
interface ApplicationReviewWithNavProps { |
109
|
|
|
application: Application; |
110
|
|
|
reviewStatusOptions: SelectOption[]; |
111
|
|
|
onStatusChange: ( |
112
|
|
|
applicationId: number, |
113
|
|
|
statusId: number | null, |
114
|
|
|
) => Promise<void>; |
115
|
|
|
onNotesChange: (applicationId: number, notes: string | null) => void; |
116
|
|
|
isSaving: boolean; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
interface ApplicationReviewWithNavState { |
120
|
|
|
selectedStatusId: number | undefined; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
class ApplicationReviewWithNav extends React.Component< |
124
|
|
|
ApplicationReviewWithNavProps & WrappedComponentProps, |
125
|
|
|
ApplicationReviewWithNavState |
126
|
|
|
> { |
127
|
|
|
public constructor( |
128
|
|
|
props: ApplicationReviewWithNavProps & WrappedComponentProps, |
129
|
|
|
) { |
130
|
|
|
super(props); |
131
|
|
|
this.state = { |
132
|
|
|
selectedStatusId: |
133
|
|
|
props.application.application_review && |
134
|
|
|
props.application.application_review.review_status_id |
135
|
|
|
? props.application.application_review.review_status_id |
136
|
|
|
: undefined, |
137
|
|
|
}; |
138
|
|
|
this.handleStatusChange = this.handleStatusChange.bind(this); |
139
|
|
|
this.handleSaveClicked = this.handleSaveClicked.bind(this); |
140
|
|
|
this.showNotes = this.showNotes.bind(this); |
141
|
|
|
this.handleLinkClicked = this.handleLinkClicked.bind(this); |
142
|
|
|
this.isUnchanged = this.isUnchanged.bind(this); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Returns true only if selectedStatusId matches the review |
147
|
|
|
* status of props.application |
148
|
|
|
*/ |
149
|
|
|
protected isUnchanged = (): boolean => { |
150
|
|
|
const { application } = this.props; |
151
|
|
|
const { selectedStatusId } = this.state; |
152
|
|
|
if ( |
153
|
|
|
application.application_review && |
154
|
|
|
application.application_review.review_status_id |
155
|
|
|
) { |
156
|
|
|
return ( |
157
|
|
|
application.application_review.review_status_id === selectedStatusId |
158
|
|
|
); |
159
|
|
|
} |
160
|
|
|
return selectedStatusId === undefined; |
161
|
|
|
}; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* When save is clicked, it is only necessary to save the status |
165
|
|
|
* @param event |
166
|
|
|
*/ |
167
|
|
|
protected handleSaveClicked(): Promise<void> { |
168
|
|
|
const { selectedStatusId } = this.state; |
169
|
|
|
const { application, onStatusChange, intl } = this.props; |
170
|
|
|
const status = selectedStatusId || null; |
171
|
|
|
|
172
|
|
|
const sectionChange = ( |
173
|
|
|
oldStatus: number | null, |
174
|
|
|
newStatus: number | null, |
175
|
|
|
): boolean => { |
176
|
|
|
const oldIsScreenedOut: boolean = |
177
|
|
|
oldStatus === ReviewStatusId.ScreenedOut; |
178
|
|
|
const newIsScreenedOut: boolean = |
179
|
|
|
newStatus === ReviewStatusId.ScreenedOut; |
180
|
|
|
return oldIsScreenedOut !== newIsScreenedOut; |
181
|
|
|
}; |
182
|
|
|
const oldStatus = application.application_review |
183
|
|
|
? application.application_review.review_status_id |
184
|
|
|
: null; |
185
|
|
|
if (sectionChange(oldStatus, status)) { |
186
|
|
|
const confirmText = |
187
|
|
|
status === ReviewStatusId.ScreenedOut |
188
|
|
|
? intl.formatMessage(messages.screenOutConfirm) |
189
|
|
|
: intl.formatMessage(messages.screenInConfirm); |
190
|
|
|
return Swal.fire({ |
191
|
|
|
title: confirmText, |
192
|
|
|
icon: "question", |
193
|
|
|
showCancelButton: true, |
194
|
|
|
confirmButtonColor: "#0A6CBC", |
195
|
|
|
cancelButtonColor: "#F94D4D", |
196
|
|
|
confirmButtonText: intl.formatMessage(messages.confirmButton), |
197
|
|
|
cancelButtonText: intl.formatMessage(messages.cancelButton), |
198
|
|
|
}).then(result => { |
199
|
|
|
if (result.value) { |
200
|
|
|
return onStatusChange(application.id, status); |
201
|
|
|
} |
202
|
|
|
return Promise.resolve(); |
203
|
|
|
}); |
204
|
|
|
} |
205
|
|
|
return onStatusChange(application.id, status); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
protected showNotes(): void { |
209
|
|
|
const { application, onNotesChange, intl } = this.props; |
210
|
|
|
const notes = |
211
|
|
|
application.application_review && application.application_review.notes |
212
|
|
|
? application.application_review.notes |
213
|
|
|
: ""; |
214
|
|
|
Swal.fire({ |
215
|
|
|
title: intl.formatMessage(messages.editNote), |
216
|
|
|
icon: "question", |
217
|
|
|
input: "textarea", |
218
|
|
|
showCancelButton: true, |
219
|
|
|
confirmButtonColor: "#0A6CBC", |
220
|
|
|
cancelButtonColor: "#F94D4D", |
221
|
|
|
cancelButtonText: intl.formatMessage(messages.cancelButton), |
222
|
|
|
confirmButtonText: intl.formatMessage(messages.save), |
223
|
|
|
inputValue: notes, |
224
|
|
|
}).then(result => { |
225
|
|
|
if (result && result.value !== undefined) { |
226
|
|
|
const value = result.value ? result.value : null; |
227
|
|
|
onNotesChange(application.id, value); |
228
|
|
|
} |
229
|
|
|
}); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
protected handleLinkClicked(url: string): void { |
233
|
|
|
if (this.isUnchanged()) { |
234
|
|
|
window.location.href = url; |
235
|
|
|
return; |
236
|
|
|
} |
237
|
|
|
this.handleSaveClicked() |
238
|
|
|
.then(() => { |
239
|
|
|
window.location.href = url; |
240
|
|
|
}) |
241
|
|
|
.catch(() => { |
242
|
|
|
// do not navigate away from page |
243
|
|
|
}); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
protected handleStatusChange( |
247
|
|
|
event: React.ChangeEvent<HTMLSelectElement>, |
248
|
|
|
): void { |
249
|
|
|
const value = |
250
|
|
|
event.target.value && !Number.isNaN(Number(event.target.value)) |
251
|
|
|
? Number(event.target.value) |
252
|
|
|
: undefined; |
253
|
|
|
this.setState({ selectedStatusId: value }); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
public render(): React.ReactElement { |
257
|
|
|
const { application, reviewStatusOptions, isSaving, intl } = this.props; |
258
|
|
|
const l10nReviewStatusOptions = reviewStatusOptions.map(status => ({ |
259
|
|
|
value: status.value, |
260
|
|
|
label: intl.formatMessage(messages[status.label]), |
261
|
|
|
})); |
262
|
|
|
const { selectedStatusId } = this.state; |
263
|
|
|
const reviewStatus = |
264
|
|
|
application.application_review && |
265
|
|
|
application.application_review.review_status |
266
|
|
|
? application.application_review.review_status.name |
267
|
|
|
: null; |
268
|
|
|
const statusIconClass = className("fas", { |
269
|
|
|
"fa-ban": reviewStatus === "screened_out", |
270
|
|
|
"fa-question-circle": reviewStatus === "still_thinking", |
271
|
|
|
"fa-check-circle": reviewStatus === "still_in", |
272
|
|
|
"fa-exclamation-circle": reviewStatus === null, |
273
|
|
|
}); |
274
|
|
|
|
275
|
|
|
const getSaveButtonText = (): string => { |
276
|
|
|
if (isSaving) { |
277
|
|
|
return intl.formatMessage(messages.saving); |
278
|
|
|
} |
279
|
|
|
if (this.isUnchanged()) { |
280
|
|
|
return intl.formatMessage(messages.saved); |
281
|
|
|
} |
282
|
|
|
return intl.formatMessage(messages.save); |
283
|
|
|
}; |
284
|
|
|
const saveButtonText = getSaveButtonText(); |
285
|
|
|
const noteButtonText = |
286
|
|
|
application.application_review && application.application_review.notes |
287
|
|
|
? intl.formatMessage(messages.editNote) |
288
|
|
|
: intl.formatMessage(messages.addNote); |
289
|
|
|
return ( |
290
|
|
|
<div> |
291
|
|
|
<div> |
292
|
|
|
<div className="manager-application-preview-actions flex-grid"> |
293
|
|
|
<div className="box small-1of3"> |
294
|
|
|
<button |
295
|
|
|
className="button--blue light-bg" |
296
|
|
|
type="button" |
297
|
|
|
onClick={() => |
298
|
|
|
this.handleLinkClicked( |
299
|
|
|
routes.managerJobApplications( |
300
|
|
|
intl.locale, |
301
|
|
|
application.job_poster_id, |
302
|
|
|
), |
303
|
|
|
) |
304
|
|
|
} |
305
|
|
|
> |
306
|
|
|
<FormattedMessage |
307
|
|
|
id="apl.backToApplicantList" |
308
|
|
|
defaultMessage="< Save and Go Back to Applicant List" |
309
|
|
|
description="Back Button text" |
310
|
|
|
/> |
311
|
|
|
</button> |
312
|
|
|
</div> |
313
|
|
|
<div className="box small-2of3"> |
314
|
|
|
<button |
315
|
|
|
className="button--blue light-bg" |
316
|
|
|
type="button" |
317
|
|
|
onClick={() => |
318
|
|
|
this.handleLinkClicked( |
319
|
|
|
routes.managerJobShow( |
320
|
|
|
intl.locale, |
321
|
|
|
application.job_poster_id, |
322
|
|
|
), |
323
|
|
|
) |
324
|
|
|
} |
325
|
|
|
> |
326
|
|
|
<FormattedMessage |
327
|
|
|
id="button.viewJobPoster" |
328
|
|
|
defaultMessage="View Job Poster" |
329
|
|
|
description="View Job Poster Button text" |
330
|
|
|
/> |
331
|
|
|
</button> |
332
|
|
|
<button |
333
|
|
|
className="button--blue light-bg" |
334
|
|
|
data-button-type="expand-all" |
335
|
|
|
type="button" |
336
|
|
|
id="expand-all" |
337
|
|
|
> |
338
|
|
|
<span className="expand"> |
339
|
|
|
{" "} |
340
|
|
|
<FormattedMessage |
341
|
|
|
id="apl.expandAllSkills" |
342
|
|
|
defaultMessage="Expand All Skills" |
343
|
|
|
description="Expand All Skills Button text" |
344
|
|
|
/> |
345
|
|
|
</span> |
346
|
|
|
<span className="collapse"> |
347
|
|
|
{" "} |
348
|
|
|
<FormattedMessage |
349
|
|
|
id="apl.collapseAllSkills" |
350
|
|
|
defaultMessage="Collapse All Skills" |
351
|
|
|
description="Collapse All Skills Button text" |
352
|
|
|
/> |
353
|
|
|
</span> |
354
|
|
|
</button> |
355
|
|
|
</div> |
356
|
|
|
</div> |
357
|
|
|
</div> |
358
|
|
|
|
359
|
|
|
<form className="applicant-summary"> |
360
|
|
|
<div className="flex-grid middle gutter"> |
361
|
|
|
<div className="box lg-1of11 applicant-status"> |
362
|
|
|
<i className={statusIconClass} /> |
363
|
|
|
</div> |
364
|
|
|
|
365
|
|
|
<div className="box lg-2of11 applicant-information"> |
366
|
|
|
<span className="name"> |
367
|
|
|
{application.applicant.user.first_name}{" "} |
368
|
|
|
{application.applicant.user.last_name} |
369
|
|
|
</span> |
370
|
|
|
<a |
371
|
|
|
href={`mailto: ${application.applicant.user.email}`} |
372
|
|
|
title={intl.formatMessage(messages.emailCandidate)} |
373
|
|
|
> |
374
|
|
|
{application.applicant.user.email} |
375
|
|
|
</a> |
376
|
|
|
{/* This span only shown for veterans */} |
377
|
|
|
{(application.veteran_status.name === "current" || |
378
|
|
|
application.veteran_status.name === "past") && ( |
379
|
|
|
<span className="veteran-status"> |
380
|
|
|
<img |
381
|
|
|
alt={intl.formatMessage(messages.viewApplicationTitle)} |
382
|
|
|
src="/images/icon_veteran.svg" |
383
|
|
|
/>{" "} |
384
|
|
|
<FormattedMessage |
385
|
|
|
id="veteranStatus.veteran" |
386
|
|
|
defaultMessage="Veteran" |
387
|
|
|
description="Veteran" |
388
|
|
|
/> |
389
|
|
|
</span> |
390
|
|
|
)} |
391
|
|
|
</div> |
392
|
|
|
|
393
|
|
|
<div className="box lg-2of11 applicant-links"> |
394
|
|
|
<a |
395
|
|
|
title={intl.formatMessage(messages.viewApplicationTitle)} |
396
|
|
|
href={routes.managerApplicationShow( |
397
|
|
|
intl.locale, |
398
|
|
|
application.id, |
399
|
|
|
)} |
400
|
|
|
> |
401
|
|
|
<i className="fas fa-file-alt" /> |
402
|
|
|
<FormattedMessage |
403
|
|
|
id="apl.viewApplication" |
404
|
|
|
defaultMessage="View Application" |
405
|
|
|
description="Button text View Application" |
406
|
|
|
/> |
407
|
|
|
</a> |
408
|
|
|
<a |
409
|
|
|
title={intl.formatMessage(messages.viewProfileTitle)} |
410
|
|
|
href={routes.managerApplicantShow( |
411
|
|
|
intl.locale, |
412
|
|
|
application.applicant_id, |
413
|
|
|
)} |
414
|
|
|
> |
415
|
|
|
<i className="fas fa-user" /> |
416
|
|
|
<FormattedMessage |
417
|
|
|
id="apl.viewProfile" |
418
|
|
|
defaultMessage="View Profile" |
419
|
|
|
description="Button text View Profile" |
420
|
|
|
/> |
421
|
|
|
</a> |
422
|
|
|
</div> |
423
|
|
|
|
424
|
|
|
<div className="box lg-2of11 applicant-decision" data-clone> |
425
|
|
|
<Select |
426
|
|
|
id={`review_status_${application.id}`} |
427
|
|
|
name="review_status" |
428
|
|
|
label={intl.formatMessage(messages.decision)} |
429
|
|
|
required={false} |
430
|
|
|
selected={selectedStatusId || null} |
431
|
|
|
nullSelection={intl.formatMessage(messages.notReviewed)} |
432
|
|
|
options={l10nReviewStatusOptions} |
433
|
|
|
onChange={this.handleStatusChange} |
434
|
|
|
/> |
435
|
|
|
</div> |
436
|
|
|
|
437
|
|
|
<div className="box lg-2of11 applicant-notes"> |
438
|
|
|
<button |
439
|
|
|
className="button--outline" |
440
|
|
|
type="button" |
441
|
|
|
onClick={this.showNotes} |
442
|
|
|
> |
443
|
|
|
{noteButtonText} |
444
|
|
|
</button> |
445
|
|
|
</div> |
446
|
|
|
|
447
|
|
|
<div className="box lg-2of11 applicant-save-button"> |
448
|
|
|
<button |
449
|
|
|
className="button--blue light-bg" |
450
|
|
|
type="button" |
451
|
|
|
onClick={() => this.handleSaveClicked()} |
452
|
|
|
> |
453
|
|
|
<span>{saveButtonText}</span> |
454
|
|
|
</button> |
455
|
|
|
</div> |
456
|
|
|
</div> |
457
|
|
|
</form> |
458
|
|
|
</div> |
459
|
|
|
); |
460
|
|
|
} |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
export default injectIntl(ApplicationReviewWithNav); |
464
|
|
|
|